﻿// INGENIOUS FRAMEWORK - Semantic Web Development Simplified
// Copyright (C) 2012 Linked Data Tools

// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.

// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.

// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.Linq;
using System.Xml;

using Ingenious.Graph;

namespace Ingenious.Owl.Linq
{
    /// <summary>
    /// Parses RDF in RDF/XML format into a binary triple model
    /// </summary>
    public class RdfXmlParser : RdfParserBase
    {
        #region Constants
        private const string LineInformationFormat = " line {0}, position {1}";
        #endregion

        private XDocument m_document;
        private string m_owlResourceNamespace = null;

        #region Object Lifetime
        public RdfXmlParser(string fileName)
            : base(fileName)
        {
        }

        public RdfXmlParser(Uri webAddress)
            : base(webAddress)
        {
        }
        #endregion

        #region Helper Methods
        private string GetLineInfo(XElement element)
        {
            IXmlLineInfo lineInfo = (IXmlLineInfo)element;
            return string.Format(LineInformationFormat, lineInfo.LineNumber, lineInfo.LinePosition);
        }
        #endregion

        private void ValidateElement(XElement element)
        {            
            if (ValidQualifiedNames.ContainsKey(element.Name.NamespaceName))
            {
                if (!ValidQualifiedNames[element.Name.NamespaceName].Contains(element.Name.LocalName))
                {
                    throw new ValidationException("Unexpected element '" + element.Name.LocalName + "' in namespace '" + element.Name.NamespaceName + "'");
                }
            }
                            
            foreach (XElement childElement in element.Elements())
            {
                ValidateElement(childElement);
            }
        }

        private void ValidateElements()
        {
            ValidateElement(m_document.Root);
        }

        protected override void ValidateContent(string rdfContent)
        {
            // Check for valid RDF/XML
            try
            {
                // Load with line info being set, so that we can report back on where
                // errors occur in the document to the user
                m_document = XDocument.Parse(rdfContent, LoadOptions.SetLineInfo);
            }
            catch(XmlException ex)
            {
                throw new ValidationException("Badly formed XML ('" + ex.Message + "')", ex);
            }

            // Verify we have the correct node name
            if (m_document.Root.Name.LocalName != RdfHelper.RdfLocalName)
            {
                throw new ValidationException("Unexpected root node name '" + m_document.Root.Name.LocalName + "'. Expected '" + RdfHelper.RdfLocalName + "' for an RDF document");
            }

            // Verify we have the RDF namespace required to defined the root node
            string rdfPrefix = m_document.Root.GetPrefixOfNamespace(XNamespace.Get(RdfHelper.RdfXmlNamespace));

            if (string.IsNullOrEmpty(rdfPrefix))
            {
                throw new ValidationException("Expected RDF namespace '" + RdfHelper.RdfXmlNamespace + "' not found on root node");
            }            
        }

        private void ParseStatementContainer(XElement statementContainer)
        {
            XAttribute subjectUriAttribute = statementContainer.Attribute(XName.Get(RdfHelper.AboutAttributeName, RdfHelper.RdfXmlNamespace));

            if (subjectUriAttribute != null)
            {
                if (string.IsNullOrEmpty(subjectUriAttribute.Value))
                {
                    throw new ParseException("Expected subject URI not given for statement container" + GetLineInfo(statementContainer));
                }

                Uri subjectUri = new Uri(subjectUriAttribute.Value, UriKind.Absolute);

                foreach (XElement childElement in statementContainer.Nodes())
                {
                    // Form the predicate Uri
                    Uri predicateUri = new Uri(childElement.Name.NamespaceName + childElement.Name.LocalName, UriKind.Absolute);

                    // Now check if we have a datatype (literal) or a resource (object reference)
                    XAttribute objectUriAttribute = childElement.Attribute(XName.Get(RdfHelper.ResourceAttributeName, RdfHelper.RdfXmlNamespace));
                    
                    Triple triple = null;

                    if (objectUriAttribute != null)
                    {
                        // We have an object property
                        triple = new Triple(subjectUri, predicateUri, new Uri(objectUriAttribute.Value));
                    }
                    else
                    {
                        // We have a literal property

                        // If we have an xsd type, make sure we parse it as this type
                        XAttribute dataTypeAttribute = childElement.Attribute(XName.Get(RdfHelper.DataTypeAttributeName, RdfHelper.RdfXmlNamespace));

                        if (dataTypeAttribute == null)
                        {
                            // Just treat as a string
                            triple = new Triple(subjectUri, predicateUri, childElement.Value);
                        }
                        else
                        {
                            Type type = null;

                            if (!XsdHelper.TryGetTypeForSchemaType(dataTypeAttribute.Value, out type))
                            {
                                throw new ParseException("Unrecognised data type '" + dataTypeAttribute.Value + "'");
                            }

                            // Now need to convert to the corresponding type

                        }
                    }

                    Triples.Add(triple);
                }
            }
            else
            {
                throw new ParseException("Statement container defined without subject URI" + GetLineInfo(statementContainer));
            }
        }

        private void ParseOwlClass(XElement owlClassElement)
        {
            // Note: See http://www.w3.org/TR/owl-ref/#ClassDescription for how the OWL class
            // definition parses as an RDF triple

            // We either expect an rdf:ID or rdf:about attribute here. In the about case, we expect
            // a namespace matching that given in the ontology header. In the ID case, we append the
            // ontology namespace with that given in the header with the ID
            
            // Add the triple represented by the OWL class declaration
            Triples.Add(new Triple(new Uri(m_owlResourceNamespace),
                new Uri(RdfHelper.RdfXmlNamespace + RdfHelper.TypeLocalName),
                new Uri(OwlHelper.OwlXmlNamespace + OwlHelper.ClassLocalName)));
        }

        protected override void ParseContent(string rdfContent)
        {
            XElement rdfElement = m_document.Root;

            // Pre-parse the OWL header in the document, if present, as the
            // ontology namespace will be required to construct triples for
            // other OWL entities
            XElement ontologyElement = rdfElement.Element(XName.Get(OwlHelper.OntologyLocalName, OwlHelper.OwlXmlNamespace));

            if (ontologyElement != null)
            {
                XAttribute aboutAttribute = ontologyElement.Attribute(XName.Get(RdfHelper.AboutAttributeName, RdfHelper.RdfXmlNamespace));

                if (aboutAttribute != null)
                {
                    m_owlResourceNamespace = aboutAttribute.Value;

                    if (string.IsNullOrEmpty(m_owlResourceNamespace))
                    {
                        throw new ParseException("OWL header namespace declaration is not permitted to be blank" + GetLineInfo(ontologyElement));
                    }
                }
                else
                {
                    throw new ParseException("Ontology header element '" + OwlHelper.OwlXmlNamespace + OwlHelper.OntologyLocalName + "' found without expected URI declaration" + GetLineInfo(ontologyElement));
                }
            }

            foreach (XElement element in rdfElement.Nodes())
            {
                if (element.Name.NamespaceName == RdfHelper.RdfXmlNamespace)
                {
                    // These are RDF namespace elements we expect as children of the root RDF element
                    switch (element.Name.LocalName)
                    {
                    case RdfHelper.DescriptionLocalName:
                        {
                            // A statement container, containing triples with a common subject
                            ParseStatementContainer(element);
                        }
                        break;
                    default:
                        {
                            throw new ParseException("Unexpected element '" + element.Name.LocalName + "' in namespace '" + RdfHelper.RdfXmlNamespace + "'" + GetLineInfo(element));
                        }
                    }
                }
                else if (element.Name.NamespaceName == OwlHelper.OwlXmlNamespace)
                {
                    if (string.IsNullOrEmpty(m_owlResourceNamespace))
                    {
                        // These are OWL namespace elements we expect as children of the root RDF element
                        switch (element.Name.LocalName)
                        {
                            case OwlHelper.ClassLocalName:
                                {
                                    // OWL class definition
                                    ParseOwlClass(element);
                                }
                                break;
                            default:
                                {
                                    throw new ParseException("Unexpected element '" + element.Name.LocalName + "' in namespace '" + OwlHelper.OwlXmlNamespace + "'" + GetLineInfo(element));
                                }
                        }
                    }
                    else
                    {
                        throw new ParseException("OWL element '" + element.Name.LocalName + "' encountered without OWL header declaration in document" + GetLineInfo(element));
                    }
                }
            }
        }
    }
}
